Tensor
Tensor
(API) is the N-D array implementation in Serrano, just like NDArray in NumPy
.
Create a Tensor object
In the API of Tensor
, there lists several initializations.
- Below code creates a tensor with shape
[255, 255, 3]
and set all elements' values to0.3
:// dimension info of tensor let shape = TensorShape(dataType: .float, shape: [255, 255, 3] ) let tensor = Tensor(repeatingValue: 0.3, tensorShape: shape)
- Creating a tensor object from a Swift array:
let data = [ [1.0, 0.5, 1.3], [2.0, 4.2, 6.7], ] let shape = TensorShape(dataType: .float, shape [2, 3] ) let tensor = Tensor(dataArray: data, tensorShape: shape)
Loading Util APIs
Serrano plans to support more convenient data loading APIs and welcome contribution.
Shape of a Tensor
A tensor object has an attribute shape
indicating this tensor object's dimension information.
Struct TensorShape
(API) is defined in Serrano to represent shape information.
Creating TensorShape
TensorShape
is defined as a struct
. It can be created directly like:
let shapeA = TensorShape(dataType: .float, shape [255, 255, 3]) let shapeB = TensorShape(dataType: .int, shape [10, 3])
TensorShape
has two attributes:
- dataType: a
TensorDataType
variable - shape
[Int]
array
dataType:
Enum TensorDataType
has int
, float
and double
cases.
However, inside tensor objects all values are stored as Float
values.
The dataType
of a TensorShape
just help user understand what initial data type the shape represents for.
shapeArray:
In Serrano, we follow row-marjor order to store and access elements in a Tensor object and each row is represented as an array.
For example, a Tensor object with shape [2, 3], it can be visulized as a nested array like below:
// shape [2, 3] [ [1.0, 0.5, 1.3], [2.0, 4.2, 6.7], ]
[3, image_hight, image_width]
(channel first):
[ // R channel frame [ [232, ..., 123], // (# of Int elements) = image_width . . . [113, ..., 225] ], // (# of Array elements) = image_hight // G channel frame [ [232, ..., 123], . . . [113, ..., 225] ], // B channel frame [ [232, ..., 123], . . . [113, ..., 225] ] ]
Rank 0
as scalar
If a tensor shape object with 0
rank, i.e.: shape.ShapeArray.count == 0
, it means the shape represent a scalar variable.
Equatable of TensorShapes:
- Two
TensorShape
objects are equal (==
) if theirshapeArray
attributes are equal. - Two
TensorShape
objects are dot equal (.==
) if they have the sameshapeArray
and samedataType
. Example:let shapeA = TensorShape(dataType: .float, shape [255, 255, 3]) let shapeB = TensorShape(dataType: .int, shape [255, 255, 3]) let shapeC = TensorShape(dataType: .float, shape [255, 255, 3]) shapeA == shapeB // true shapeA .== shapeB // false shapeA .== shapeC // true
Memory layout
When user create a Tensor object, Serrano will manually allocate a continuous memory space for this Tensor object to store its values. So basically, inside a Tensor object it stores the N-D array as a flattened vector.
Page-aligned allocation
Serrano uses posix_memalign
(man) to allocate the continuous memory.
We allocated page-alinged memory so that later when using Metal GPU, we do not need to copy values to construct a MTLBuffer. Details check here.
Under Improvement
This memory allocation strategy is under improvement. May change in future.
Access memory
floatValueReader
(API) allow users to access allocated memory.
It is a UnsafeMutableBufferPointer<Float>
pointer. User can access and modify a single element like:
let t = Tensor(repeatingValue: 0.2, tensorShape: TensorShape(dataType: .float, shape [3, 2]) ) let reader = t.floatValueReader print(reader[0]) // '0.2' reader[0] = 1.0 print(reader[0]) // '1.0'
Tensor Slice
Sometimes, user may want to access part of a tensor object. Tensor class has slice(sliceIndex:[Int])
(API) function which could slice part of a tensor into a new tensor object.
For example we have a tensor with shape [3, 2, 2]
and we think the first dimension as channel, next two dimensions are height and width.
let dataArray = [ // 1st channel [ [0.5, 2.6], [1.1, 9.3], ], // 2nd channel [ [2.7, 4.1], [6.3, 1.7], ], // 3rd channel [ [5.6, 3.2], [1.9, 9.1], ], ] let rootTensor = Tensor(dataArray: dataArray, tensorShape: TensorShape(dataType: .float, shape: [3, 2, 2]) )
If you want to get the 1st channel's information:
let sliceOne = rootTensor.slice([0]) print(slice.shape.shapeArray) // [2, 2] print(slice.nestedArrayFloat()) /** Print out: [ [0.5, 2.3], [1.1, 9.3], ] */
If you want to get the 1st row in 2nd channel:
let sliceTwo = rootTensor.slice([1, 1]) print(slice.shape.shapeArray) // [2] print(slice.nestedArrayFloat()) /** Print out: [2.7, 4.1] */
Sliced tensor can also be used to slice again:
let sliceThree = sliceOne.slice([0]) print(sliceThree.shape.shapeArray) // [2] print(sliceThree.nestedArrayFloat()) /** Print out: [0.5, 2.3] */
Sliced Tensor Holds a Strong Reference to root Tensor
A sliced Tensor share memory of its tensor object. And it holds a strong reference to root tensor. Keep an eye on this.
Under Improvement
Slice related APIs are under improvement. More useful functions are adding up.